package de.htwg_konstanz.ebus.framework.impEx;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.StringReader;
import java.math.BigDecimal;
import java.util.List;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Source;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import de.htwg_konstanz.ebus.framework.view.ImpExStatusModel;
import de.htwg_konstanz.ebus.framework.wholesaler.api.bo.BOAddress;
import de.htwg_konstanz.ebus.framework.wholesaler.api.bo.BOCountry;
import de.htwg_konstanz.ebus.framework.wholesaler.api.bo.BOProduct;
import de.htwg_konstanz.ebus.framework.wholesaler.api.bo.BOSalesPrice;
import de.htwg_konstanz.ebus.framework.wholesaler.api.bo.BOSupplier;
import de.htwg_konstanz.ebus.framework.wholesaler.api.boa.AddressBOA;
import de.htwg_konstanz.ebus.framework.wholesaler.api.boa.CountryBOA;
import de.htwg_konstanz.ebus.framework.wholesaler.api.boa.PriceBOA;
import de.htwg_konstanz.ebus.framework.wholesaler.api.boa.ProductBOA;
import de.htwg_konstanz.ebus.framework.wholesaler.api.boa.SupplierBOA;
import de.htwg_konstanz.ebus.framework.wholesaler.vo.util.Constants;


/**
 * Implementation to provide XML import functionality
 */

//TODO: fix exception handling. throw generic "XML parse exception" to controller.
//TODO: add log4j instead of sysouts.

public class XmlImporter implements IImporter {
	private ImpExStatusModel statusModel = null;
	
	private BOSupplier defaultSupplier = null;
	private XPath xpath = null;
	
	Schema schema = null;
	Document doc = null;
	
	/*
	 * XPath paths to elements, relative from <ARTICLES>-axis
	 */
	final static String XPATH_DESCRIPTION_LONG = "ARTICLE_DETAILS/DESCRIPTION_LONG";
	final static String XPATH_DESCRIPTION_SHORT = "ARTICLE_DETAILS/DESCRIPTION_SHORT";
	final static String XPATH_EAN = "ARTICLE_DETAILS/EAN";
	
	final static String XPATH_SUPPLIER_NAME = "/BMECAT/HEADER/SUPPLIER/SUPPLIER_NAME";
	final static String XPATH_PRICES = "ARTICLE_PRICE_DETAILS/ARTICLE_PRICE";
	final static String XPATH_ARTICLES = "/BMECAT/T_NEW_CATALOG/ARTICLE";
	
	final static String XPATH_PRICES_TERRITORIES = "TERRITORY";
	final static String XPATH_PRICE_AMOUNT = "PRICE_AMOUNT";
	final static String XPATH_PRICE_CURRENCY = "PRICE_CURRENCY";
	final static String XPATH_PRICE_TAX = "TAX";
	final static String XPATH_PRICE_TYPE = "@price_type";	
	
	/**
	 * Constructor. Load XML inputSource and schema and create a Document instance to work with.
	 * @param schemaPath
	 * @param dataPath
	 */
	public XmlImporter(String schemaPath, InputSource dataPath) {
		initialize(schemaPath, dataPath);
	}
	
	
	/**
	 * Constructor. Load XML file and schema and create a Document instance to work with.
	 * @param schemaPath
	 * @param dataPath
	 */
	public XmlImporter(String schemaPath, String dataPath) {
		//load file in dataPath, convert to InputSource
		//call default method
		
		//remove namespace from bmecat-node.
		String xmlContent = null;
		try {
			xmlContent = readFileAsString(dataPath);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		xmlContent = xmlContent.replace("xmlns:ns2=\"http://www.htwg-konstanz.de/ebsf/ws/product_catalog\"", "");
		xmlContent = xmlContent.replace("xmlns=\"http://www.bmecat.org/XMLSchema/1.2/bmecat_new_catalog\"", "");

		//convert xmlContent to inputSource
		InputSource inputSource = new InputSource(new StringReader(xmlContent));
		initialize(schemaPath, inputSource);
	}
	
	
	public void initialize(String schemaPath, InputSource data) {
		statusModel = new ImpExStatusModel();
		statusModel.setImport();
		statusModel.setWellformed(true);
		
		SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
		Source schemaFile = new StreamSource(new File(schemaPath));
		try {
			schema = schemaFactory.newSchema(schemaFile);
		} catch (SAXException e1) {
			System.out.println("Schema is invalid - " + e1.getMessage());
		}
		
		//check only for wellformed-ness
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder  = null;
		try {
			factory.setValidating(false);
			factory.setNamespaceAware(true);
			builder = factory.newDocumentBuilder();
			
			doc = builder.parse(data);
			
		} catch (ParserConfigurationException e) {
			e.printStackTrace();
		} catch (SAXException e) {
			System.out.println("Parsing failed SAXException" + e.getMessage());
			statusModel.setWellformed(false);
			statusModel.setErrorCause(e.getMessage());
		} catch (IOException e) {
			statusModel.setWellformed(false);
			statusModel.setErrorCause(e.getMessage());
		}
		
		xpath = XPathFactory.newInstance().newXPath();
	}
	
	/**
	 * Validate a given XML file to a schema
	 */
	public boolean isValid() {
		if(doc == null) {
			System.out.println("Invalid because no data available (doc is null)");
			statusModel.setValid(false);
			return false;
		}		
		Validator validator = schema.newValidator();
		try {
			validator.validate(new DOMSource(doc));
		} catch (Exception e) {
			System.out.println("Invalid because " + e.getMessage());
			statusModel.setValid(false);
			statusModel.setErrorCause(e.getMessage());
			return false;
		}
		statusModel.setValid(true);
		return true;
	}

	/**
	 * Perform the actual import
	 */
	public void performImport() {
		//create or use an existing supplier for this import
		BOCountry country = CountryBOA.getInstance().findCountry("DE");
		
		//make sure no duplicated suppliers or addresses are created
		BOSupplier supplier = null;
		String companyName = null;
		try {
			companyName = xpath.evaluate(XPATH_SUPPLIER_NAME, doc);
			//Node node = (Node) xpath.evaluate(XPATH_SUPPLIER_NAME, doc, XPathConstants.NODE);
			//companyName = node.getTextContent();
		} catch (XPathExpressionException e) {
			System.out.println("couldn't find supplier's company name." + e.getMessage());
		}
		
		List<BOSupplier> suppliers = SupplierBOA.getInstance().findAll();
		for (BOSupplier existingSupplier : suppliers) {
			if (existingSupplier.getCompanyname().equals(companyName)) {
				supplier = existingSupplier;
			}
		}
		
		if (supplier == null) {
			BOAddress address = new BOAddress();
			address.setStreet("unknown Street");
			address.setZipcode("00000");
			address.setCity("unknown City");
			address.setCountry(country);
			AddressBOA.getInstance().saveOrUpdate(address);
			
			supplier = new BOSupplier();
			supplier.setAddress(address);
			supplier.setFirstname("unknown firstname");
			supplier.setLastname("unknown lastname");
			supplier.setCompanyname(companyName);
		}
		
		this.defaultSupplier = supplier;
		SupplierBOA.getInstance().saveOrUpdate(supplier);
		
		
		//find all ARTICLE nodes and create the appropiate objects		
		NodeList articles = null;
		try {
			articles = (NodeList) xpath.evaluate(XPATH_ARTICLES, doc, XPathConstants.NODESET);
		} catch (XPathExpressionException e) {
			System.out.println("unable to find articles in xml!");
		}
		
		if (articles != null) {
			if (articles.getLength() != 0) {
				System.out.println("found " + articles.getLength() + " to import.");
			} else {
				statusModel.setProcessedArticles(0);
				
				System.out.println("no articles found");
			}
		
			for (int i = 0; i < articles.getLength(); i++) {
				Node currentNode = articles.item(i);
				System.out.print("* importing article... ");
				retrieveArticleDetails(currentNode);
			}
		}
		
		System.out.println("*** finished import.");
	}
	
	/**
	 * create a single product from article data
	 * @param articleNode
	 */
	public void retrieveArticleDetails(Node articleNode) {
		BOProduct product = new BOProduct();
	
		try {
			statusModel.setProcessedArticles(statusModel.getProcessedArticles()+1);
			
			Node node = (Node) xpath.evaluate(XPATH_DESCRIPTION_LONG, articleNode, XPathConstants.NODE);
			product.setLongDescription(node.getTextContent());
			
			node = (Node) xpath.evaluate(XPATH_DESCRIPTION_SHORT, articleNode, XPathConstants.NODE);
			product.setShortDescription(node.getTextContent());

			product.setSupplier(defaultSupplier);

			// essential attributes we do not have. 
			node = (Node) xpath.evaluate(XPATH_EAN, articleNode, XPathConstants.NODE);
			if (node != null) {
				product.setOrderNumberSupplier("XS" + node.getTextContent());
				product.setOrderNumberCustomer("XC" + node.getTextContent());
			} else {
				//FIXME: added because EAN is not available when importing from webservice
				product.setOrderNumberSupplier("XS" + (int)Math.random()*1000);
				product.setOrderNumberCustomer("XC" + (int)Math.random()*1000);
			}
			
			//check if this product already exists: 
			//if it does only perform update. do not create a new product
			List<BOProduct> sameProducts = ProductBOA.getInstance().findByShortdescription(product.getShortDescription());
			if (sameProducts != null && sameProducts.size() > 0) {
				System.out.println("product " + product.getShortDescription() + " already exists. updating.");
				statusModel.setUpdatedArticles(statusModel.getUpdatedArticles()+1);
				//still use old (referencing!) attributes from the old product.
				product.setMaterialNumber(sameProducts.get(0).getMaterialNumber());	
				product.setOrderNumberCustomer(sameProducts.get(0).getOrderNumberCustomer());
				product.setOrderNumberSupplier(sameProducts.get(0).getOrderNumberSupplier());
			}
			statusModel.addArticleName(product.getShortDescription());
			ProductBOA.getInstance().saveOrUpdate(product);
			
			//set prizes
			NodeList set = (NodeList) xpath.evaluate(XPATH_PRICES, articleNode, XPathConstants.NODESET);
			//for each ARTICLE_PRICE node
			for (int i = 0; i < set.getLength(); i++) {
				Node priceNode = set.item(i);
				
				Node priceDetailNode = (Node) xpath.evaluate(XPATH_PRICE_AMOUNT, priceNode, XPathConstants.NODE);
				String priceAmount = priceDetailNode.getTextContent();
				
				priceDetailNode = (Node) xpath.evaluate(XPATH_PRICE_CURRENCY, priceNode, XPathConstants.NODE);
				String priceCurrency = priceDetailNode.getTextContent();
				
				priceDetailNode = (Node) xpath.evaluate(XPATH_PRICE_TAX, priceNode, XPathConstants.NODE);
				String tax = priceDetailNode.getTextContent();
				
				//for each TERRITORY node
				NodeList territories = (NodeList) xpath.evaluate(XPATH_PRICES_TERRITORIES, priceNode, XPathConstants.NODESET);
				for (int j = 0; j < territories.getLength(); j++) {
					//create actual price object and persist it
					Node territory = territories.item(j);
					BOSalesPrice price = new BOSalesPrice();
					price.setAmount(new BigDecimal(priceAmount));
					price.setPricetype(priceNode.getAttributes().getNamedItem("price_type").getTextContent());
					
					//convert double of type 0.19 to integer 19
					Double t = new Double(tax);
					t = t * 100;
					price.setTaxrate(new Integer(t.intValue()));
					
					price.setProduct(product);
					
					BOCountry country = CountryBOA.getInstance().findCountry(territory.getTextContent());
					price.setCountry(country);
					price.setLowerboundScaledprice(Constants.DEFAULT_LOWERBOUND_SCALED_PRICE);
				
					PriceBOA.getInstance().saveOrUpdateSalesPrice(price);
				}
				
				// TODO: import ARTICLE_ORDER_DETAILS
				//OrderItemBOA.getInstance()
				
				
				
			}
		} catch (XPathExpressionException e) {
			//abort import for this article
			System.out.println("fail " + e.getMessage());
			statusModel.setFailedArticles(statusModel.getFailedArticles()+1);
			statusModel.setErrorCause(e.getMessage());
			return;
		} finally {
			//clean up
		}
		System.out.println("success");
	}

	public ImpExStatusModel getStatusModel() {
		return statusModel;
	}
	
    private String readFileAsString(String filePath) throws java.io.IOException{
        StringBuffer fileData = new StringBuffer(1000);
        BufferedReader reader = new BufferedReader(
                new FileReader(filePath));
        char[] buf = new char[1024];
        int numRead=0;
        while((numRead=reader.read(buf)) != -1){
            String readData = String.valueOf(buf, 0, numRead);
            fileData.append(readData);
            buf = new char[1024];
        }
        reader.close();
        return fileData.toString();
    }

}
